// SPDX-License-Identifier: GPL-2.0

/*
 * lombo_n7v3.c - Common Machine for LomboTech SoCs
 *
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <sound/soc.h>
#include <linux/iio/iio.h>
#include <sound/pcm_params.h>

#include <mach/debug.h>

#undef  MOD_NAME
#define MOD_NAME "LOMBO_N7V3"
#define AUDIO_PLL_48000S		(24576000)
#define AUDIO_PLL_44100S		(22579200)

#define DEFAULT_WSS			(32)

struct lombo_power_amp {
	int shutdown;         /* gpio for pa */
	int pa_enable_value;
	int pa_disable_value;
	int wakeup_delay;

	int gpadc_channel;
};

static int lombo_amp_event(struct snd_soc_dapm_widget *widget,
	struct snd_kcontrol *kctl, int event)
{
	int ret;
	struct snd_soc_card *card = widget->dapm->card;
	struct snd_soc_pcm_runtime *rtd;
	struct snd_soc_component *component;
	struct lombo_power_amp *pa = snd_soc_card_get_drvdata(card);

	rtd = snd_soc_get_pcm_runtime(card, card->dai_link[0].name);
	component = rtd->codec_dai->component;

	if (SND_SOC_DAPM_EVENT_ON(event)) {
		if (pa->shutdown >= 0) {
			ret = gpio_direction_output(pa->shutdown,
					pa->pa_enable_value);
			if (ret < 0)
				dev_err(component->dev, "set pa power on err\n");
		} else {
			if (pa->gpadc_channel >= 0)
				lombo_write_ainx_output(pa->gpadc_channel, 1);
		}

		if (pa->wakeup_delay)
			msleep(pa->wakeup_delay);
	} else {
		if (pa->shutdown >= 0) {
			ret = gpio_direction_output(pa->shutdown,
					pa->pa_disable_value);
			if (ret < 0)
				dev_err(component->dev, "set pa power off err\n");
		} else {
			if (pa->gpadc_channel >= 0)
				lombo_write_ainx_output(pa->gpadc_channel, 0);
		}
	}

	return 0;
}

/* use subseq here to make sure pa on after lineout */
#define LB_SOC_DAPM_SPK(wname, wevent) \
{	.id = snd_soc_dapm_spk, .name = wname, .kcontrol_news = NULL, \
	.subseq = 20, .num_kcontrols = 0, \
	.reg = SND_SOC_NOPM, .event = wevent, \
	.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}


static const struct snd_soc_dapm_widget lombo_dapm_widgets[] = {
	LB_SOC_DAPM_SPK("Speakers", lombo_amp_event),
	SND_SOC_DAPM_MIC("DMICIN", NULL),
	SND_SOC_DAPM_MIC("AMICIN", NULL),
};

static const struct snd_kcontrol_new lombo_controls[] = {
	SOC_DAPM_PIN_SWITCH("Speakers"),
	SOC_DAPM_PIN_SWITCH("DMICIN"),
	SOC_DAPM_PIN_SWITCH("AMICIN"),
};

static int lombo_hw_params(struct snd_pcm_substream *substream,
	struct snd_pcm_hw_params *params)
{
	int ret;
	int clk_rate, clk_id;
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_dai *codec_dai = rtd->codec_dai;
	struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
	int sample_rate = params_rate(params);

	PRT_DBG("samle rate %d\n", sample_rate);
	switch (sample_rate) {
	case 8000:
	case 12000:
	case 16000:
	case 24000:
	case 32000:
	case 48000:
	case 96000:
	case 192000:
		clk_id	 = 0;
		clk_rate = AUDIO_PLL_48000S;
		break;
	case 11025:
	case 22050:
	case 44100:
	case 88200:
		clk_id	 = 1;
		clk_rate = AUDIO_PLL_44100S;
		break;
	default:
		return -EINVAL;
	}

	/* set the clk rate */
	ret = snd_soc_dai_set_sysclk(codec_dai, clk_id,
		clk_rate, SND_SOC_CLOCK_OUT);
	if (ret < 0) {
		dev_err(codec_dai->dev, "set sysclk error\n");
		return ret;
	}

	ret = snd_soc_dai_set_sysclk(cpu_dai, clk_id,
		clk_rate, SND_SOC_CLOCK_OUT);
	if (ret) {
		dev_err(cpu_dai->dev, "set sysclk error\n");
		return ret;
	}

	/* set codec m/s mode, i2s data fmt */
	ret = snd_soc_dai_set_fmt(codec_dai, rtd->dai_link->dai_fmt);
	if (ret < 0)
		return ret;

	/* set i2s m/s mode, i2s data fmt */
	ret = snd_soc_dai_set_fmt(cpu_dai, rtd->dai_link->dai_fmt);
	if (ret < 0)
		return ret;

	return 0;
}

static int lombo_dmic_hw_params(struct snd_pcm_substream *substream,
	struct snd_pcm_hw_params *params)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	int clk_rate, clk_id;
	struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
	int ret;
	int sample_rate;

	sample_rate = params_rate(params);
	PRT_DBG("samle rate %d\n", sample_rate);
	switch (sample_rate) {
	case 8000:
	case 16000:
	case 24000:
	case 32000:
	case 48000:
	case 96000:
	case 192000:
		clk_id	 = 0;
		clk_rate = AUDIO_PLL_48000S;
		break;
	case 11025:
	case 22050:
	case 44100:
	case 88200:
		clk_id	 = 1;
		clk_rate = AUDIO_PLL_44100S;
		break;
	default:
		return -EINVAL;
	}
	ret = snd_soc_dai_set_sysclk(cpu_dai, clk_id,
		clk_rate, SND_SOC_CLOCK_OUT);
	if (ret) {
		dev_err(rtd->card->dev, "%s() error setting sysclk to %u: %d\n",
				__func__, clk_rate, ret);
		return ret;
	}

	return 0;
}

/*
 * n7v3 DAI operations.
 */
static struct snd_soc_ops lombo_soc_ops = {
	.hw_params = lombo_hw_params,
};

static const struct snd_soc_ops lombo_soc_dmic_ops = {
	.hw_params = lombo_dmic_hw_params,
};

static struct snd_soc_card snd_soc_n7v3 = {
	.name = "n7v3_card",
	.owner = THIS_MODULE,
	.dapm_widgets = lombo_dapm_widgets,
	.num_dapm_widgets = ARRAY_SIZE(lombo_dapm_widgets),
	.controls = lombo_controls,
	.num_controls = ARRAY_SIZE(lombo_controls),
};

enum {
	DAILINK_AC,
	DAILINK_DMIC,
};

static const struct snd_soc_dai_link n7v3_dai_links[] = {
	[DAILINK_AC] = {
		.name            = "n7v3_ac",
		.stream_name     = "audio",
		.codec_dai_name  = "lombo_ac_dai",
		.dai_fmt         = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
					SND_SOC_DAIFMT_CBS_CFS,
		.ops		 = &lombo_soc_ops,
	},
	[DAILINK_DMIC] = {
		.name            = "dmic",
		.stream_name     = "pdm",
		/* codec_dai_name must match with dmic dai driver's name*/
		.codec_dai_name  = "dmic_dai",
		.dai_fmt         = SND_SOC_DAI_FORMAT_PDM |
							SND_SOC_DAIFMT_NB_NF |
							SND_SOC_DAIFMT_CBS_CFS,
		.ops             = &lombo_soc_dmic_ops,
	},
};

static const struct snd_soc_dapm_route lombo_sound_ac_routes[] = {
	/* Output */
	{"Speakers", NULL, "SPKL"},
	{"Speakers", NULL, "SPKR"},

	/* Input */
	{"MICN", NULL, "AMICIN"},
	{"MICP", NULL, "AMICIN"},
};

struct lombo_sound_route {
	const struct snd_soc_dapm_route *routes;
	int num_routes;
};

static const struct lombo_sound_route lombo_routes[] = {
	[DAILINK_AC] = {
		.routes = lombo_sound_ac_routes,
		.num_routes = ARRAY_SIZE(lombo_sound_ac_routes),
	},
	[DAILINK_DMIC] = {0},

};

struct lombo_dailink_match_data {
	const char *compatible;
	struct bus_type *bus_type;
};

static const struct lombo_dailink_match_data lombo_dailink_match[] = {
	[DAILINK_AC] = {
		.compatible = "lombo, n7v3-ac",
	},
	[DAILINK_DMIC] = {
		.compatible = "lombo, dmic",
	},
};

static int of_dev_node_match(struct device *dev, void *data)
{
	return dev->of_node == data;
}

static int lombo_sound_codec_node_match(struct device_node *np_codec)
{
	struct device *dev;
	int i;

	for (i = 0; i < ARRAY_SIZE(lombo_dailink_match); i++) {
		if (!of_device_is_compatible(np_codec,
					     lombo_dailink_match[i].compatible))
			continue;

		if (lombo_dailink_match[i].bus_type) {
			dev = bus_find_device(lombo_dailink_match[i].bus_type,
							NULL, np_codec,
							of_dev_node_match);
			if (!dev)
				continue;
			put_device(dev);
		}

		return i;
	}
	return -1;
}

static int lombo_sound_of_parse_dais(struct device *dev,
					struct snd_soc_card *card)
{
	struct device_node *np_cpu;
	struct device_node *np_codec;
	struct snd_soc_dai_link *dai;
	struct snd_soc_dapm_route *routes;
	int i, index;
	int num_routes;

	card->dai_link = devm_kzalloc(dev, sizeof(n7v3_dai_links),
				      GFP_KERNEL);
	if (!card->dai_link)
		return -ENOMEM;

	num_routes = 0;
	for (i = 0; i < ARRAY_SIZE(lombo_routes); i++)
		num_routes += lombo_routes[i].num_routes;
	routes = devm_kcalloc(dev, num_routes, sizeof(*routes),
			      GFP_KERNEL);
	if (!routes)
		return -ENOMEM;
	card->dapm_routes = routes;


	card->num_dapm_routes = 0;
	card->num_links = 0;
	for (i = 0; i < ARRAY_SIZE(n7v3_dai_links); i++) {
		np_codec = of_parse_phandle(dev->of_node,
					    "lombo,codec", i);
		if (!np_codec)
			break;

		if (!of_device_is_available(np_codec))
			continue;

		index = lombo_sound_codec_node_match(np_codec);
		if (index < 0)
			continue;

		np_cpu = of_parse_phandle(dev->of_node, "lombo,cpu_dai", i);

		if (!np_cpu) {
			dev_err(dev, "Missing 'lombo,cpu_dai' for %s\n",
				n7v3_dai_links[index].name);
			return -EINVAL;
		}
		if (!of_device_is_available(np_cpu))
			continue;

		dai = &card->dai_link[card->num_links++];
		*dai = n7v3_dai_links[index];

		if (!dai->codec_name)
			dai->codec_of_node = np_codec;
		dai->platform_of_node = np_cpu;
		dai->cpu_of_node = np_cpu;

		if (lombo_routes[i].num_routes <= 0)
			continue;

		if (card->num_dapm_routes + lombo_routes[index].num_routes >
		    num_routes) {
			dev_err(dev, "Too many routes\n");
			return -EINVAL;
		}

		memcpy(routes + card->num_dapm_routes,
		       lombo_routes[index].routes,
		       lombo_routes[index].num_routes * sizeof(*routes));
		card->num_dapm_routes += lombo_routes[index].num_routes;
	}

	return 0;
}

static int lombo_audio_probe(struct platform_device *pdev)
{
	int ret;
	struct snd_soc_card *card = &snd_soc_n7v3;
	struct lombo_power_amp *pa;
	/*struct device_node *np = pdev->dev.of_node;*/
	enum of_gpio_flags flag;
	int pa_vcc;

	card->dev = &pdev->dev;
	ret = lombo_sound_of_parse_dais(&pdev->dev, card);
	if (ret < 0) {
		dev_err(&pdev->dev, "Failed to parse dais: %d\n", ret);
		return ret;
	}

	pa = devm_kzalloc(&pdev->dev, sizeof(*pa), GFP_KERNEL);
	if (!pa)
		return -ENOMEM;
	/* get gpio ac_shutdown */
	pa->shutdown = of_get_named_gpio_flags(pdev->dev.of_node,
						"pa_shutdown", 0, &flag);
	if (pa->shutdown < 0)
		dev_info(&pdev->dev, "can't find ac shutdown gpio\n");
	if (gpio_is_valid(pa->shutdown)) {
		ret = devm_gpio_request(&pdev->dev,
			pa->shutdown, "pa_shutdown");
		if (ret < 0) {
			dev_err(&pdev->dev,
				"request %d fail\n", pa->shutdown);
			pa->shutdown = -1;
		}
		pa->pa_enable_value = (flag == OF_GPIO_ACTIVE_LOW) ? 0 : 1;
		pa->pa_disable_value = (!pa->pa_enable_value) & 1;
		gpio_direction_output(pa->shutdown, pa->pa_disable_value);
	}

	pa_vcc = of_get_named_gpio_flags(pdev->dev.of_node,
						"pa_vcc", 0, &flag);
	if (gpio_is_valid(pa_vcc)) {
		int vcc_en = 0;

		ret = devm_gpio_request(&pdev->dev,
			pa_vcc, "pa_vcc");
		if (ret < 0) {
			dev_err(&pdev->dev,
				"request %d fail\n", pa_vcc);
		}
		vcc_en = (flag == OF_GPIO_ACTIVE_LOW) ? 0 : 1;
		gpio_direction_output(pa_vcc, vcc_en);
	}

	device_property_read_u32(&pdev->dev, "pa-wakeup-delay-ms",
				 &pa->wakeup_delay);

	ret = device_property_read_u32(&pdev->dev, "gpadc_channel", &(pa->gpadc_channel));
	if(ret < 0)
		pa->gpadc_channel = -1;

	snd_soc_card_set_drvdata(card, pa);

	ret = snd_soc_register_card(card);
	if (ret)
		dev_err(&pdev->dev, "snd_soc_register_card() failed:%d\n", ret);

	return ret;
}

static int lombo_audio_remove(struct platform_device *pdev)
{
	return snd_soc_unregister_card(&snd_soc_n7v3);
}

static const struct of_device_id n7v3_of_match[] = {
	{ .compatible = "lombo, n7v3-audio", },
	{},
};

MODULE_DEVICE_TABLE(of, n7v3_of_match);

static struct platform_driver lombo_audio_driver = {
	.driver		= {
		.name	= "lombo-audio-n7v3",
		.of_match_table = of_match_ptr(n7v3_of_match),
		.pm	= &snd_soc_pm_ops,
	},
	.probe		= lombo_audio_probe,
	.remove		= lombo_audio_remove,
};

module_platform_driver(lombo_audio_driver);


MODULE_AUTHOR("lomboswer <lomboswer@lombotech.com>");
MODULE_DESCRIPTION("ALSA SoC n7v3");
MODULE_LICENSE("GPL");

